SDWebImage (5.0.6)图片加载奇淫巧技 这篇文章介绍了SDWebImage加载图片的流程是怎样的,本文我们一起讨论一下,SDWebImage框架的缓存机制是怎么样的。
我们先来看加载过程中,SDWebImage是如何从缓存中读取我们所需的图片的,我们先找到读取缓存的入口:1
2// Start the entry to load image from cache  
[self callCacheProcessForOperation:operation url:url options:options context:context progress:progressBlock completed:completedBlock];
这个方法就是开始从缓存中读取,方法内部实现如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27// Query cache process
- (void)callCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
url:(nonnull NSURL *)url
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock {
    // Check whether we should query cache
    BOOL shouldQueryCache = (options & SDWebImageFromLoaderOnly) == 0;
    if (shouldQueryCache) {
    id<SDWebImageCacheKeyFilter> cacheKeyFilter = context[SDWebImageContextCacheKeyFilter];
        NSString *key = [self cacheKeyForURL:url cacheKeyFilter:cacheKeyFilter];
        @weakify(operation);
        operation.cacheOperation = [self.imageCache queryImageForKey:key options:options context:context completion:^(UIImage * _Nullable cachedImage, NSData * _Nullable cachedData, SDImageCacheType cacheType) {
            @strongify(operation);
            if (!operation || operation.isCancelled) {
            [self safelyRemoveOperationFromRunning:operation];
            return;
            }
            // Continue download process
            [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:cachedImage cachedData:cachedData cacheType:cacheType progress:progressBlock completed:completedBlock];
        }];
    } else {
        // Continue download process
        [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:nil cachedData:nil cacheType:SDImageCacheTypeNone progress:progressBlock completed:completedBlock];
    }
}
从上面的源码可以看出,首先会判断一下是否需要从缓存中读取,如果不需要就直接去下载了。如果需要的话,就会使用self.imageCache实例去查询缓存,核心代码:1
2
3
4
5
6
7
8
9[self.imageCache queryImageForKey:key options:options context:context completion:^(UIImage * _Nullable cachedImage, NSData * _Nullable cachedData, SDImageCacheType cacheType) {
    @strongify(operation);
    if (!operation || operation.isCancelled) {
        [self safelyRemoveOperationFromRunning:operation];
        return;
    }
    // Continue download process
    [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:cachedImage cachedData:cachedData cacheType:cacheType progress:progressBlock completed:completedBlock];
}];
我们可以看到,查询结果回调之后,又调用了下载方法,这时因为缓存查询的结果也在下载方法里面处理了。
我们接着看查询缓存的方法:queryImageForKey: 看下它的内部实现:1
2
3
4
5
6
7
8
9
10
11- (id<SDWebImageOperation>)queryImageForKey:(NSString *)key options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context completion:(nullable SDImageCacheQueryCompletionBlock)completionBlock {
    SDImageCacheOptions cacheOptions = 0;
    if (options & SDWebImageQueryMemoryData) cacheOptions |= SDImageCacheQueryMemoryData;
    if (options & SDWebImageQueryMemoryDataSync) cacheOptions |= SDImageCacheQueryMemoryDataSync;
    if (options & SDWebImageQueryDiskDataSync) cacheOptions |= SDImageCacheQueryDiskDataSync;
    if (options & SDWebImageScaleDownLargeImages) cacheOptions |= SDImageCacheScaleDownLargeImages;
    if (options & SDWebImageAvoidDecodeImage) cacheOptions |= SDImageCacheAvoidDecodeImage;
    if (options & SDWebImageDecodeFirstFrameOnly) cacheOptions |= SDImageCacheDecodeFirstFrameOnly;
    if (options & SDWebImagePreloadAllFrames) cacheOptions |= SDImageCachePreloadAllFrames;
    return [self queryCacheOperationForKey:key options:cacheOptions context:context done:completionBlock];
}
这个方法里面做了一堆跟缓存相关的条件判断,然后调用了queryCacheOperationForKey:这个方法,正式进入缓存查询。我们接着看这个方法的源码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context done:(nullable SDImageCacheQueryCompletionBlock)doneBlock {
    if (!key) {
        if (doneBlock) {
            doneBlock(nil, nil, SDImageCacheTypeNone);
        }
    return nil;
    }
    id<SDImageTransformer> transformer = context[SDWebImageContextImageTransformer];
    if (transformer) {
        // grab the transformed disk image if transformer provided
        NSString *transformerKey = [transformer transformerKey];
        key = SDTransformedKeyForKey(key, transformerKey);
    }
    // First check the in-memory cache...
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    if ((options & SDImageCacheDecodeFirstFrameOnly) && image.sd_isAnimated) {
        #if SD_MAC
        image = [[NSImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:kCGImagePropertyOrientationUp];
        #else
        image = [[UIImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:image.imageOrientation];
        #endif
    }
    BOOL shouldQueryMemoryOnly = (image && !(options & SDImageCacheQueryMemoryData));
    if (shouldQueryMemoryOnly) {
        if (doneBlock) {
            doneBlock(image, nil, SDImageCacheTypeMemory);
        }
        return nil;
    }
    // Second check the disk cache...
    NSOperation *operation = [NSOperation new];
    // Check whether we need to synchronously query disk
    // 1. in-memory cache hit & memoryDataSync
    // 2. in-memory cache miss & diskDataSync
    BOOL shouldQueryDiskSync = ((image && options & SDImageCacheQueryMemoryDataSync) ||
    (!image && options & SDImageCacheQueryDiskDataSync));
    void(^queryDiskBlock)(void) =  ^{
        if (operation.isCancelled) {
        // do not call the completion if cancelled
        return;
    }
    @autoreleasepool {
    NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
    UIImage *diskImage;
    SDImageCacheType cacheType = SDImageCacheTypeNone;
    if (image) {
        // the image is from in-memory cache, but need image data
        diskImage = image;
        cacheType = SDImageCacheTypeMemory;
    } else if (diskData) {
        cacheType = SDImageCacheTypeDisk;
        // decode image data only if in-memory cache missed
        diskImage = [self diskImageForKey:key data:diskData options:options context:context];
        if (diskImage && self.config.shouldCacheImagesInMemory) {
            NSUInteger cost = diskImage.sd_memoryCost;
            [self.memCache setObject:diskImage forKey:key cost:cost];
        }
    }
    if (doneBlock) {
        if (shouldQueryDiskSync) {
            doneBlock(diskImage, diskData, cacheType);
        } else {
            dispatch_async(dispatch_get_main_queue(), ^{
                doneBlock(diskImage, diskData, cacheType);
            });
        }
    }
    }
    };
    // Query in ioQueue to keep IO-safe
    if (shouldQueryDiskSync) {
        dispatch_sync(self.ioQueue, queryDiskBlock);
    } else {
        dispatch_async(self.ioQueue, queryDiskBlock);
    }
    return operation;
}
接下来我们解析一下这个方法:
首先做了参数合法性判断,不满足条件直接返回空。
紧接着就是从内存缓存中查询我们需要的图片:1
2// First check the in-memory cache...
UIImage *image = [self imageFromMemoryCacheForKey:key];
这个方法比较简单,就是直接从memCache中按照key来读取: [self.memCache objectForKey:key];
接下来判断图片是否是动图,做相应的处理。
然后,判断是否是只需在内存中查找,如果是的话直接返回刚才查到的图片,并结束方法调用。如果不是的话,就去硬盘中查找。1
NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
我们先把这个流程看完再来看究竟是如何从磁盘中读取Data的。
接下来通过一系列条件得出是否需要同步查询缓存:1
2BOOL shouldQueryDiskSync = ((image && options & SDImageCacheQueryMemoryDataSync) ||
(!image && options & SDImageCacheQueryDiskDataSync));
然后生命一个磁盘查询的block: queryDiskBlock开始查询。
接下来判断是否从内存中读取到了图片,如果有的话,不做其他处理,最后回调的时候把diskData一起回调出去就可以了。如果没有从内存中读取到图片,则就要看diskData是否为空,如果不为空,就对diskData进行解码处理,得到我们需要的diskImage。1
2// decode image data only if in-memory cache missed
diskImage = [self diskImageForKey:key data:diskData options:options context:context];
然后把得到的diskImage放到内存缓存中一份。1
2
3
4if (diskImage && self.config.shouldCacheImagesInMemory) {
    NSUInteger cost = diskImage.sd_memoryCost;
    [self.memCache setObject:diskImage forKey:key cost:cost];
}
到这里,从内存以及磁盘中读取缓存的流程完毕,接下来就是把我们读取出来的图片数据返回给调用方了。1
2
3
4
5
6
7
8
9if (doneBlock) {
    if (shouldQueryDiskSync) {
        doneBlock(diskImage, diskData, cacheType);
    } else {
        dispatch_async(dispatch_get_main_queue(), ^{
            doneBlock(diskImage, diskData, cacheType);
        });
    }
}
最后就是根据是否需要同步查询来调用queryDiskBlock了。这就是图片缓存查询的流程。
接下来,我们回头看一下磁盘缓存查询究竟是如何实现的。1
NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
我们看一下这个方法的内部实现:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20- (nullable NSData *)diskImageDataBySearchingAllPathsForKey:(nullable NSString *)key {
    if (!key) {
        return nil;
    }
    NSData *data = [self.diskCache dataForKey:key];
    if (data) {
        return data;
    }
    // Addtional cache path for custom pre-load cache
    if (self.additionalCachePathBlock) {
        NSString *filePath = self.additionalCachePathBlock(key);
        if (filePath) {
            data = [NSData dataWithContentsOfFile:filePath options:self.config.diskCacheReadingOptions error:nil];
        }
    }
    return data;
}
首先是key参数判断,其次从diskCache中通过key来读取,如果磁盘缓存,内存中已经读取过的话,直接返回。否则的话,就会通过key得出磁盘缓存的路径,然后读取出来,返回给调用方。
接下来,我们一起看一下,从Server端下载完图片后,SDWebImage是如何处理的他两级(内存缓存、磁盘缓存)缓存的。同样的我们先找到保存缓存的入口方法:1
[self callStoreCacheProcessForOperation:operation url:url options:options context:context downloadedImage:downloadedImage downloadedData:downloadedData finished:finished progress:progressBlock completed:completedBlock];
这个方法的调用时机,是在从网络下载完图片的回调中。
这个方法的内部实现中有一句很重要的代码,就是我们存储缓存的代码:1
[self.imageCache storeImage:downloadedImage imageData:cacheData forKey:key cacheType:storeCacheType completion:nil];
接下来我们来看一下,这个方法的内部实现:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26- (void)storeImage:(UIImage *)image imageData:(NSData *)imageData forKey:(nullable NSString *)key cacheType:(SDImageCacheType)cacheType completion:(nullable SDWebImageNoParamsBlock)completionBlock {
switch (cacheType) {
    case SDImageCacheTypeNone: {
        [self storeImage:image imageData:imageData forKey:key toMemory:NO toDisk:NO completion:completionBlock];
    }
    break;
    case SDImageCacheTypeMemory: {
        [self storeImage:image imageData:imageData forKey:key toMemory:YES toDisk:NO completion:completionBlock];
    }
    break;
    case SDImageCacheTypeDisk: {
        [self storeImage:image imageData:imageData forKey:key toMemory:NO toDisk:YES completion:completionBlock];
    }
    break;
    case SDImageCacheTypeAll: {
        [self storeImage:image imageData:imageData forKey:key toMemory:YES toDisk:YES completion:completionBlock];
    }
    break;
    default: {
        if (completionBlock) {
            completionBlock();
        }
    }
    break;
    }
}
内部实现其实比较简单,就是根据缓存类型做相应的存储。这一句才是重点:1
[self storeImage:image imageData:imageData forKey:key toMemory:YES toDisk:YES completion:completionBlock];
我们接着往下面看:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47- (void)storeImage:(nullable UIImage *)image
imageData:(nullable NSData *)imageData
forKey:(nullable NSString *)key
toMemory:(BOOL)toMemory
toDisk:(BOOL)toDisk
completion:(nullable SDWebImageNoParamsBlock)completionBlock {
    if (!image || !key) {
        if (completionBlock) {
            completionBlock();
        }
        return;
    }
    // if memory cache is enabled
    if (toMemory && self.config.shouldCacheImagesInMemory) {
        NSUInteger cost = image.sd_memoryCost;
        [self.memCache setObject:image forKey:key cost:cost];
    }
    if (toDisk) {
        dispatch_async(self.ioQueue, ^{
            @autoreleasepool {
                NSData *data = imageData;
                if (!data && image) {
                // If we do not have any data to detect image format, check whether it contains alpha channel to use PNG or JPEG format
                SDImageFormat format;
                if ([SDImageCoderHelper CGImageContainsAlpha:image.CGImage]) {
                    format = SDImageFormatPNG;
                } else {
                    format = SDImageFormatJPEG;
                }
                    data = [[SDImageCodersManager sharedManager] encodedDataWithImage:image format:format options:nil];
                }
                [self _storeImageDataToDisk:data forKey:key];
            }
            if (completionBlock) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    completionBlock();
                });
            }
        });
    } else {
        if (completionBlock) {
            completionBlock();
        }
    }
}
接着我们分心一下存储缓存的最关键的方法,没错,就是上面的这段代码。
首先,对参数进行合法性判断,如果不合法,直接返回。否则继续向下执行:
第一步:判断是否允许内存缓存,如果允许就把下载好的图片存储在内存中一份。
第二步:判断是否允许存储到磁盘,如果允许就进入磁盘存储逻辑:
这里的存储任务是提交到了一个异步的串行队列中。
任务的具体处理逻辑是:如果imageData 不存在,但是image 有值,则对image进行归档,得到一份data,这也是我们要写入到磁盘的data。
最后,调用_storeImageDataToDisk:方法,将二进制图片数据写入到磁盘。
第三步:将结果回调出去。
我们看一下_storeImageDataToDisk:方法的内部实现:1
2
3
4
5
6
7
8// Make sure to call form io queue by caller
- (void)_storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key {
    if (!imageData || !key) {
        return;
    }
    [self.diskCache setData:imageData forKey:key];
}
比较简单,就是通过diskCache 实例来写缓存。接着我们进入diskCache类里面看下是如何写入的。
我们看下SDDiskCache里面的setData: forKey:方法实现:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20- (void)setData:(NSData *)data forKey:(NSString *)key {
    NSParameterAssert(data);
    NSParameterAssert(key);
    if (![self.fileManager fileExistsAtPath:self.diskCachePath]) {
        [self.fileManager createDirectoryAtPath:self.diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
    }
    // get cache Path for image key
    NSString *cachePathForKey = [self cachePathForKey:key];
    // transform to NSUrl
    NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
    [data writeToURL:fileURL options:self.config.diskCacheWritingOptions error:nil];
    // disable iCloud backup
    if (self.config.shouldDisableiCloud) {
        // ignore iCloud backup resource value error
        [fileURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil];
    }
}
其实这个也很简单,就是简单的文件写入。
到这里,SDWebImage缓存的读写机制就介绍完了,欢迎大家勘误,SDWebImage这个库比较强大,里面还有很多细节,文中都没有提到,这里只是做了主流程的介绍,读者要是想深入的理解,还得是去阅读源码,建议的阅读的时候,和本文一起读,有助于理解源码。
下一篇文章,会介绍SDWebImage是如何对图片进行编码解码的,欢迎大家阅读。
 
        